// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of '../protoc.dart';

/// Generates the Dart enum corresponding to a oneof declaration.
///
/// The enum is used to represent the state of a oneof when using the
/// corresponding which-method.
class OneofEnumGenerator {
  static void generate(
    IndentingWriter out,
    OneofNames oneof,
    List<ProtobufField> fields,
    List<int> parentPath,
  ) {
    final enumName = oneof.oneofEnumName;
    out.addAnnotatedBlock(
      'enum $enumName {',
      '}\n',
      [
        NamedLocation(
          name: enumName,
          fieldPathSegment: Paths.buildOneofPath(parentPath, oneof),
          start: 'enum '.length,
        ),
      ],
      () {
        for (final field in fields) {
          final name = oneofEnumMemberName(field.memberNames!.fieldName);
          out.printlnAnnotated('$name, ', [
            NamedLocation(
              name: name,
              fieldPathSegment: Paths.buildFieldPath(parentPath, field),
              start: 0,
            ),
          ]);
        }
        out.println('notSet');
      },
    );
  }
}

class MessageGenerator extends ProtobufContainer {
  /// The name of the Dart class to generate.
  @override
  final String classname;

  /// The fully-qualified name of the message (without any leading '.').
  @override
  final String fullName;

  /// The part of the fully qualified name that comes after the package prefix.
  ///
  /// For nested messages this will include the names of the parents.
  ///
  /// For example:
  /// ```
  /// package foo;
  ///
  /// message Container {
  ///   message Nested {
  ///     int32 int32_value = 1;
  ///   }
  /// }
  /// ```
  /// The nested message will have a `fullName` of 'foo.Container.Nested', and a
  /// `messageName` of 'Container.Nested'.
  String get messageName =>
      fullName.substring(package.isEmpty ? 0 : package.length + 1);

  PbMixin? mixin;

  @override
  final ProtobufContainer parent;

  @override
  final FeatureSet features;

  final DescriptorProto _descriptor;
  final List<EnumGenerator> _enumGenerators = <EnumGenerator>[];
  final List<MessageGenerator> _messageGenerators = <MessageGenerator>[];
  final List<ExtensionGenerator> _extensionGenerators = <ExtensionGenerator>[];

  /// Stores the list of fields belonging to each oneof declaration identified
  /// by the index in the containing types's oneof_decl list.
  /// Only contains the 'real' oneofs.
  final List<List<ProtobufField>> _oneofFields;
  final List<FeatureSet> _oneofFeatures;
  late List<OneofNames> _oneofNames;

  @override
  final List<int> fieldPath;

  // populated by resolve()
  late List<ProtobufField> fieldList;
  bool _resolved = false;

  Set<String> _usedTopLevelNames;

  MessageGenerator._(
    DescriptorProto descriptor,
    this.parent,
    Map<String, PbMixin> declaredMixins,
    PbMixin? defaultMixin,
    this._usedTopLevelNames,
    this.fieldPath,
  ) : _descriptor = descriptor,
      classname = messageOrEnumClassName(
        descriptor.name,
        _usedTopLevelNames,
        parent: parent.classname ?? '',
      ),
      fullName =
          parent.fullName == ''
              ? descriptor.name
              : '${parent.fullName}.${descriptor.name}',
      _oneofFields = List.generate(
        countRealOneofs(descriptor),
        (int index) => [],
      ),
      _oneofFeatures = List.generate(
        countRealOneofs(descriptor),
        (int index) => FeatureSet(),
      ),
      features = resolveFeatures(parent.features, descriptor.options.features) {
    mixin = _getMixin(declaredMixins, defaultMixin);
    for (var i = 0; i < _descriptor.enumType.length; i++) {
      final e = _descriptor.enumType[i];
      _enumGenerators.add(EnumGenerator.nested(e, this, _usedTopLevelNames, i));
    }

    for (var i = 0; i < _descriptor.nestedType.length; i++) {
      final n = _descriptor.nestedType[i];
      _messageGenerators.add(
        MessageGenerator.nested(
          n,
          this,
          declaredMixins,
          defaultMixin,
          _usedTopLevelNames,
          i,
        ),
      );
    }

    for (var oneof = 0; oneof < _oneofFeatures.length; oneof++) {
      _oneofFeatures[oneof] = resolveFeatures(
        features,
        descriptor.oneofDecl[oneof].options.features,
      );
    }

    // Extensions within messages won't create top-level classes and don't need
    // to check against / be added to top-level reserved names.
    final usedExtensionNames = {...forbiddenExtensionNames};
    for (var i = 0; i < _descriptor.extension.length; i++) {
      final x = _descriptor.extension[i];
      _extensionGenerators.add(
        ExtensionGenerator.nested(x, this, usedExtensionNames, i),
      );
    }
  }

  MessageGenerator.topLevel(
    DescriptorProto descriptor,
    ProtobufContainer parent,
    Map<String, PbMixin> declaredMixins,
    PbMixin? defaultMixin,
    Set<String> usedNames,
    int repeatedFieldIndex,
  ) : this._(
        descriptor,
        parent,
        declaredMixins,
        defaultMixin,
        usedNames,
        Paths.buildTopLevelMessagePath(parent.fieldPath, repeatedFieldIndex),
      );

  MessageGenerator.nested(
    DescriptorProto descriptor,
    ProtobufContainer parent,
    Map<String, PbMixin> declaredMixins,
    PbMixin? defaultMixin,
    Set<String> usedNames,
    int repeatedFieldIndex,
  ) : this._(
        descriptor,
        parent,
        declaredMixins,
        defaultMixin,
        usedNames,
        Paths.buildNestedMessagePath(parent.fieldPath, repeatedFieldIndex),
      );

  @override
  String get package => parent.package;

  /// The generator of the .pb.dart file that will declare this type.
  @override
  FileGenerator get fileGen => parent.fileGen!;

  /// Throws an exception if [resolve] hasn't been called yet.
  void checkResolved() {
    if (!_resolved) {
      throw StateError('message not resolved: $fullName');
    }
  }

  /// Returns a const expression that evaluates to the JSON for this message.
  ///
  /// [context] represents the .pb.dart file where the expression will be used.
  String getJsonConstant(FileGenerator context) {
    final name = '$classname\$json';
    if (context.protoFileUri == fileGen.protoFileUri) {
      return name;
    }
    return '${importPrefix(context: context)}.$name';
  }

  /// Adds all mixins used in this message and any submessages.
  void addMixinsTo(Set<PbMixin> output) {
    if (mixin != null) {
      output.addAll(mixin!.findMixinsToApply());
    }
    for (final m in _messageGenerators) {
      m.addMixinsTo(output);
    }
  }

  // Registers message and enum types that can be used elsewhere.
  void register(GenerationContext ctx) {
    ctx.registerFieldType(this);
    for (final m in _messageGenerators) {
      m.register(ctx);
    }
    for (final e in _enumGenerators) {
      e.register(ctx);
    }
  }

  // Creates fields and resolves extension targets.
  void resolve(GenerationContext ctx) {
    if (_resolved) throw StateError('message already resolved');
    _resolved = true;

    final reserved = mixin?.findReservedNames() ?? const <String>[];
    final members = messageMemberNames(
      _descriptor,
      classname,
      _usedTopLevelNames,
      reserved: reserved,
      lowercaseGroupNames: false,
    );

    fieldList = <ProtobufField>[];
    for (final names in members.fieldNames) {
      final descriptor = names.descriptor;
      ProtobufField field;
      if (descriptor.hasOneofIndex() && !descriptor.proto3Optional) {
        field = ProtobufField.message(
          names,
          this,
          _oneofFeatures[descriptor.oneofIndex],
          ctx,
        );
        _oneofFields[field.descriptor.oneofIndex].add(field);
      } else {
        field = ProtobufField.message(names, this, features, ctx);
      }
      fieldList.add(field);
    }
    _oneofNames = members.oneofNames;

    for (final m in _messageGenerators) {
      m.resolve(ctx);
    }
    for (final x in _extensionGenerators) {
      x.resolve(ctx);
    }
  }

  bool get needsFixnumImport {
    checkResolved();
    for (final field in fieldList) {
      if (field.needsFixnumImport) return true;
    }
    for (final m in _messageGenerators) {
      if (m.needsFixnumImport) return true;
    }
    for (final x in _extensionGenerators) {
      if (x.needsFixnumImport) return true;
    }
    return false;
  }

  /// Adds dependencies of [generate] to [imports].
  ///
  /// For each .pb.dart file that the generated code needs to import,
  /// add its generator.
  void addImportsTo(
    Set<FileGenerator> imports,
    Set<FileGenerator> enumImports,
  ) {
    checkResolved();
    for (final field in fieldList) {
      final typeGen = field.baseType.generator;
      if (typeGen is EnumGenerator) {
        enumImports.add(typeGen.fileGen!);
      } else if (typeGen != null) {
        imports.add(typeGen.fileGen!);
      }
    }
    for (final m in _messageGenerators) {
      m.addImportsTo(imports, enumImports);
    }
    for (final x in _extensionGenerators) {
      x.addImportsTo(imports, enumImports);
    }
  }

  // Returns the number of enums in this message and all nested messages.
  int get enumCount {
    var count = _enumGenerators.length;
    for (final m in _messageGenerators) {
      count += m.enumCount;
    }
    return count;
  }

  /// Adds dependencies of [generateConstants] to [imports].
  ///
  /// For each .pbjson.dart file that the generated code needs to import,
  /// add its generator.
  void addConstantImportsTo(Set<FileGenerator> imports) {
    checkResolved();
    for (final m in _messageGenerators) {
      m.addConstantImportsTo(imports);
    }
    for (final x in _extensionGenerators) {
      x.addConstantImportsTo(imports);
    }
  }

  void generate(IndentingWriter out) {
    checkResolved();

    for (final m in _messageGenerators) {
      // Don't output the generated map entry type. Instead, the `PbMap` type
      // from the protobuf library is used to hold the keys and values.
      if (m._descriptor.options.hasMapEntry()) continue;
      m.generate(out);
    }

    for (final oneof in _oneofNames) {
      OneofEnumGenerator.generate(
        out,
        oneof,
        _oneofFields[oneof.index],
        fieldPath,
      );
    }

    var mixinClause = '';
    if (mixin != null) {
      final mixinNames = mixin!.findMixinsToApply().map(
        (m) => '$mixinImportPrefix.${m.name}',
      );
      mixinClause = ' with ${mixinNames.join(", ")}';
    }

    final omitMessageNames = ConditionalConstDefinition('omit_message_names');
    out.addSuffix(
      omitMessageNames.constFieldName,
      omitMessageNames.constDefinition,
    );

    final conditionalPackageName =
        'const $protobufImportPrefix.PackageName'
        '(${omitMessageNames.createTernary(package)})';

    final packageClause =
        package == '' ? '' : ', package: $conditionalPackageName';
    final proto3JsonClause =
        (mixin?.wellKnownType != null)
            ? ', wellKnownType: $mixinImportPrefix.WellKnownType.${mixin!.wellKnownType}'
            : '';

    final String extendedClass;
    if (_descriptor.options.messageSetWireFormat) {
      extendedClass = '\$_MessageSet';
    } else {
      extendedClass = 'GeneratedMessage';
    }

    final commentBlock = fileGen.commentBlock(fieldPath);
    if (commentBlock != null) {
      out.println(commentBlock);
    }
    if (_descriptor.options.deprecated) {
      out.println(
        '@$coreImportPrefix.Deprecated(\'This message is deprecated\')',
      );
    }
    out.addAnnotatedBlock(
      'class $classname extends $protobufImportPrefix.$extendedClass$mixinClause {',
      '}',
      [
        NamedLocation(
          name: classname,
          fieldPathSegment: fieldPath,
          start: 'class '.length,
        ),
      ],
      () {
        _generateFactory(out);

        out.println();

        out.printlnAnnotated('$classname._();', [
          NamedLocation(name: classname, fieldPathSegment: fieldPath, start: 0),
        ]);

        out.println();

        out.println(
          'factory $classname.fromBuffer($coreImportPrefix.List<$coreImportPrefix.int> data,'
          ' [$protobufImportPrefix.ExtensionRegistry registry = $protobufImportPrefix.ExtensionRegistry.EMPTY])'
          ' => create()..mergeFromBuffer(data, registry);',
        );
        out.println(
          'factory $classname.fromJson($coreImportPrefix.String json,'
          ' [$protobufImportPrefix.ExtensionRegistry registry = $protobufImportPrefix.ExtensionRegistry.EMPTY])'
          ' => create()..mergeFromJson(json, registry);',
        );

        out.println();
        for (final oneof in _oneofNames) {
          out.addBlock(
            'static const $coreImportPrefix.Map<$coreImportPrefix.int, ${oneof.oneofEnumName}> ${oneof.byTagMapName} = {',
            '};',
            () {
              for (final field in _oneofFields[oneof.index]) {
                final oneofMemberName = oneofEnumMemberName(
                  field.memberNames!.fieldName,
                );
                out.println(
                  '${field.number} : ${oneof.oneofEnumName}.$oneofMemberName,',
                );
              }
              out.println('0 : ${oneof.oneofEnumName}.notSet');
            },
          );
        }

        final omitMessageNames = ConditionalConstDefinition(
          'omit_message_names',
        );
        out.addSuffix(
          omitMessageNames.constFieldName,
          omitMessageNames.constDefinition,
        );

        out.addBlock(
          'static final $protobufImportPrefix.BuilderInfo _i = '
              '$protobufImportPrefix.BuilderInfo(${omitMessageNames.createTernary(messageName)}'
              '$packageClause'
              ', createEmptyInstance: create'
              '$proto3JsonClause)',
          ';',
          () {
            for (var oneof = 0; oneof < _oneofFields.length; oneof++) {
              final tags =
                  _oneofFields[oneof]
                      .map((ProtobufField f) => f.number)
                      .toList();
              out.println('..oo($oneof, $tags)');
            }

            for (final field in fieldList) {
              field.generateBuilderInfoCall(out, package);
            }

            if (_descriptor.extensionRange.isNotEmpty) {
              out.println('..hasExtensions = true');
            }
            if (!_hasRequiredFields(this, {})) {
              out.println('..hasRequiredFields = false');
            }
          },
        );

        for (final x in _extensionGenerators) {
          x.generate(out);
        }

        out.println();
        out.println(
          '@$coreImportPrefix.Deprecated('
          "'See https://github.com/google/protobuf.dart/issues/998.')",
        );
        out.println('$classname clone() => deepCopy();');
        out.println(
          '@$coreImportPrefix.Deprecated('
          "'See https://github.com/google/protobuf.dart/issues/998.')",
        );
        out.println(
          '$classname copyWith(void Function($classname) updates) =>'
          ' super.copyWith((message) => updates(message as $classname))'
          ' as $classname;',
        );

        out.println();
        out.println('@$coreImportPrefix.override');
        out.println('$protobufImportPrefix.BuilderInfo get info_ => _i;');

        // Factory functions which can be used as default value closures.
        out.println();
        out.println("@$coreImportPrefix.pragma('dart2js:noInline')");
        out.println('static $classname create() => $classname._();');
        out.println('@$coreImportPrefix.override');
        out.println('$classname createEmptyInstance() => create();');

        out.println(
          'static $protobufImportPrefix.PbList<$classname> createRepeated() =>'
          ' $protobufImportPrefix.PbList<$classname>();',
        );
        out.println("@$coreImportPrefix.pragma('dart2js:noInline')");
        out.println(
          'static $classname getDefault() =>'
          ' _defaultInstance ??='
          ' $protobufImportPrefix.GeneratedMessage.\$_defaultFor<$classname>'
          '(create);',
        );
        out.println('static $classname? _defaultInstance;');

        generateFieldsAccessorsMutators(out);
        mixin?.injectHelpers(out);
      },
    );
    out.println();
  }

  void _generateFactory(IndentingWriter out) {
    if (!fileGen.options.disableConstructorArgs && fieldList.isNotEmpty) {
      out.println('factory $classname({');
      for (final field in fieldList) {
        _emitDeprecatedIf(field.isDeprecated, out);
        if (field.isRepeated && !field.isMapField) {
          out.println(
            '  ${field.baseType.getRepeatedDartTypeIterable(fileGen)}? ${field.memberNames!.fieldName},',
          );
        } else if (field.isMapField) {
          final keyType = field.getDartMapKeyType();
          final valueType = field.getDartMapValueType();
          out.println(
            '  $coreImportPrefix.Iterable<$coreImportPrefix.MapEntry<$keyType, $valueType>>? '
            '${field.memberNames!.fieldName},',
          );
        } else {
          out.println(
            '  ${field.getDartType()}? ${field.memberNames!.fieldName},',
          );
        }
      }
      out.print('}) ');

      final names = fieldList.map((f) => f.memberNames!.fieldName).toSet();
      var result = 'result';
      if (names.contains(result)) {
        result += r'$';
      }
      out.addBlock('{', '}', () {
        out.println('final $result = create();');
        for (final field in fieldList) {
          out.print('if (${field.memberNames!.fieldName} != null) ');
          if (field.isRepeated && !field.isMapField) {
            out.println(
              '$result.${field.memberNames!.fieldName}.addAll(${field.memberNames!.fieldName});',
            );
          } else if (field.isMapField) {
            out.println(
              '$result.${field.memberNames!.fieldName}.addEntries(${field.memberNames!.fieldName});',
            );
          } else {
            out.println(
              '$result.${field.memberNames!.fieldName} = ${field.memberNames!.fieldName};',
            );
          }
        }
        out.println('return $result;');
      });
    } else {
      out.println('factory $classname() => create();');
    }
  }

  // Returns true if the message type has any required fields.  If it doesn't,
  // we can optimize out calls to its isInitialized()/_findInvalidFields()
  // methods.
  //
  // already_seen is used to avoid checking the same type multiple times
  // (and also to protect against unbounded recursion).
  bool _hasRequiredFields(MessageGenerator type, Set<String> alreadySeen) {
    checkResolved();

    if (alreadySeen.contains(type.fullName)) {
      // The type is already in cache.  This means that either:
      // a. The type has no required fields.
      // b. We are in the midst of checking if the type has required fields,
      //    somewhere up the stack.  In this case, we know that if the type
      //    has any required fields, they'll be found when we return to it,
      //    and the whole call to HasRequiredFields() will return true.
      //    Therefore, we don't have to check if this type has required fields
      //    here.
      return false;
    }
    alreadySeen.add(type.fullName);
    // If the type has extensions, an extension with message type could contain
    // required fields, so we have to be conservative and assume such an
    // extension exists.
    if (type._descriptor.extensionRange.isNotEmpty) {
      return true;
    }

    for (final field in type.fieldList) {
      if (field.isRequired) {
        return true;
      }
      if (field.baseType.isMessage) {
        final child = field.baseType.generator as MessageGenerator;
        if (_hasRequiredFields(child, alreadySeen)) {
          return true;
        }
      }
    }
    return false;
  }

  void generateFieldsAccessorsMutators(IndentingWriter out) {
    for (final oneof in _oneofNames) {
      generateOneofAccessors(out, oneof);
    }

    for (final field in fieldList) {
      out.println();
      generateFieldAccessorsMutators(
        field,
        out,
        Paths.buildFieldPath(fieldPath, field),
      );
    }
  }

  void generateOneofAccessors(IndentingWriter out, OneofNames oneof) {
    out.println();
    for (final field in _oneofFields[oneof.index]) {
      _emitIndexAnnotation(field.number, out);
    }
    out.printlnAnnotated(
      '${oneof.oneofEnumName} ${oneof.whichOneofMethodName}() '
      '=> ${oneof.byTagMapName}[\$_whichOneof(${oneof.index})]!;',
      [
        NamedLocation(
          name: oneof.whichOneofMethodName,
          fieldPathSegment: Paths.buildOneofPath(fieldPath, oneof),
          start: '${oneof.oneofEnumName} '.length,
        ),
      ],
    );
    for (final field in _oneofFields[oneof.index]) {
      _emitIndexAnnotation(field.number, out);
    }
    out.printlnAnnotated(
      'void ${oneof.clearMethodName}() '
      '=> \$_clearField(\$_whichOneof(${oneof.index}));',
      [
        NamedLocation(
          name: oneof.clearMethodName,
          fieldPathSegment: Paths.buildOneofPath(fieldPath, oneof),
          start: 'void '.length,
        ),
      ],
    );
  }

  void generateFieldAccessorsMutators(
    ProtobufField field,
    IndentingWriter out,
    List<int> memberFieldPath,
  ) {
    final fieldTypeString = field.getDartType();
    final defaultExpr = field.getDefaultExpr();
    final names = field.memberNames;

    final commentBlock = fileGen.commentBlock(memberFieldPath);
    if (commentBlock != null) {
      out.println(commentBlock);
    }

    _emitDeprecatedIf(field.isDeprecated, out);
    _emitOverrideIf(field.overridesGetter, out);
    _emitIndexAnnotation(field.number, out);
    final getterExpr = _getterExpression(
      fieldTypeString,
      field.index!,
      defaultExpr,
      field.isRepeated,
      field.isMapField,
    );

    out.printlnAnnotated(
      '$fieldTypeString get ${names!.fieldName} => $getterExpr;',
      [
        NamedLocation(
          name: names.fieldName,
          fieldPathSegment: memberFieldPath,
          start: '$fieldTypeString get '.length,
        ),
      ],
    );

    if (field.isRepeated) {
      if (field.overridesSetter) {
        throw 'Field ${field.fullName} cannot override a setter for '
            '${names.fieldName} because it is repeated.';
      }
      if (field.overridesHasMethod) {
        throw 'Field ${field.fullName} cannot override '
            '${names.hasMethodName}() because it is repeated.';
      }
      if (field.overridesClearMethod) {
        throw 'Field ${field.fullName} cannot override '
            '${names.clearMethodName}() because it is repeated.';
      }
    } else {
      final fastSetter = field.baseType.setter;
      _emitDeprecatedIf(field.isDeprecated, out);
      _emitOverrideIf(field.overridesSetter, out);
      _emitIndexAnnotation(field.number, out);
      if (fastSetter != null) {
        out.printlnAnnotated(
          'set ${names.fieldName}($fieldTypeString value) => '
          '$fastSetter(${field.index}, value);',
          [
            NamedLocation(
              name: names.fieldName,
              fieldPathSegment: memberFieldPath,
              start: 'set '.length,
            ),
          ],
        );
      } else {
        out.printlnAnnotated(
          'set ${names.fieldName}($fieldTypeString value) => '
          '\$_setField(${field.number}, value);',
          [
            NamedLocation(
              name: names.fieldName,
              fieldPathSegment: memberFieldPath,
              start: 'set '.length,
            ),
          ],
        );
      }
      if (field.hasPresence) {
        _emitDeprecatedIf(field.isDeprecated, out);
        _emitOverrideIf(field.overridesHasMethod, out);
        _emitIndexAnnotation(field.number, out);
        out.printlnAnnotated(
          '$coreImportPrefix.bool ${names.hasMethodName}() =>'
          ' \$_has(${field.index});',
          [
            NamedLocation(
              name: names.hasMethodName!,
              fieldPathSegment: memberFieldPath,
              start: '$coreImportPrefix.bool '.length,
            ),
          ],
        );
      }
      _emitDeprecatedIf(field.isDeprecated, out);
      _emitOverrideIf(field.overridesClearMethod, out);
      _emitIndexAnnotation(field.number, out);
      out.printlnAnnotated(
        'void ${names.clearMethodName}() =>'
        ' \$_clearField(${field.number});',
        [
          NamedLocation(
            name: names.clearMethodName!,
            fieldPathSegment: memberFieldPath,
            start: 'void '.length,
          ),
        ],
      );
      if (field.baseType.isMessage) {
        _emitDeprecatedIf(field.isDeprecated, out);
        _emitIndexAnnotation(field.number, out);
        out.printlnAnnotated(
          '$fieldTypeString ${names.ensureMethodName}() => '
          '\$_ensure(${field.index});',
          <NamedLocation>[
            NamedLocation(
              name: names.ensureMethodName!,
              fieldPathSegment: memberFieldPath,
              start: '$fieldTypeString '.length,
            ),
          ],
        );
      }
    }
  }

  String _getterExpression(
    String fieldType,
    int index,
    String defaultExpr,
    bool isRepeated,
    bool isMapField,
  ) {
    if (isMapField) {
      return '\$_getMap($index)';
    }
    if (fieldType == '$coreImportPrefix.String') {
      if (defaultExpr == '""' || defaultExpr == "''") {
        return '\$_getSZ($index)';
      }
      return '\$_getS($index, $defaultExpr)';
    }
    if (fieldType == '$coreImportPrefix.bool') {
      if (defaultExpr == 'false') {
        return '\$_getBF($index)';
      }
      return '\$_getB($index, $defaultExpr)';
    }
    if (fieldType == '$coreImportPrefix.int') {
      if (defaultExpr == '0') {
        return '\$_getIZ($index)';
      }
      return '\$_getI($index, $defaultExpr)';
    }
    if (fieldType == '$fixnumImportPrefix.Int64' && defaultExpr == 'null') {
      return '\$_getI64($index)';
    }
    if (defaultExpr == 'null') {
      return isRepeated ? '\$_getList($index)' : '\$_getN($index)';
    }
    return '\$_get($index, $defaultExpr)';
  }

  void _emitDeprecatedIf(bool condition, IndentingWriter out) {
    if (condition) {
      out.println(
        '@$coreImportPrefix.Deprecated(\'This field is deprecated.\')',
      );
    }
  }

  void _emitOverrideIf(bool condition, IndentingWriter out) {
    if (condition) {
      out.println('@$coreImportPrefix.override');
    }
  }

  void _emitIndexAnnotation(int index, IndentingWriter out) {
    out.println('@$protobufImportPrefix.TagNumber($index)');
  }

  void generateEnums(IndentingWriter out) {
    for (final e in _enumGenerators) {
      e.generate(out);
    }

    for (final m in _messageGenerators) {
      m.generateEnums(out);
    }
  }

  /// Writes a Dart constant containing the JSON for the ProtoDescriptor.
  /// Also writes a separate constant for each nested message,
  /// to avoid duplication.
  void generateConstants(IndentingWriter out) {
    const nestedTypeTag = 3;
    const enumTypeTag = 4;
    assert(_descriptor.info_.fieldInfo[nestedTypeTag]!.name == 'nestedType');
    assert(_descriptor.info_.fieldInfo[enumTypeTag]!.name == 'enumType');

    final name = getJsonConstant(fileGen);
    final json = _descriptor.writeToJsonMap();
    final nestedTypeNames =
        _messageGenerators.map((m) => m.getJsonConstant(fileGen)).toList();
    final nestedEnumNames =
        _enumGenerators.map((e) => e.getJsonConstant(fileGen)).toList();

    out.println(
      '@$coreImportPrefix.Deprecated'
      '(\'Use ${toplevelParent!.binaryDescriptorName} instead\')',
    );
    out.addBlock('const $name = {', '};', () {
      for (final key in json.keys) {
        out.print("'$key': ");
        if (key == '$nestedTypeTag') {
          // refer to message constants by name instead of repeating each value
          out.println("[${nestedTypeNames.join(", ")}],");
          continue;
        } else if (key == '$enumTypeTag') {
          // refer to enum constants by name
          out.println("[${nestedEnumNames.join(", ")}],");
          continue;
        }
        writeJsonConst(out, json[key]);
        out.println(',');
      }
    });
    out.println();

    for (final m in _messageGenerators) {
      m.generateConstants(out);
    }

    for (final e in _enumGenerators) {
      e.generateConstants(out);
    }
  }

  /// Returns the mixin for this message, or null if none.
  ///
  /// First searches [_wellKnownMixins], then [declaredMixins],
  /// then internal mixins declared by [findMixin].
  PbMixin? _getMixin(
    Map<String, PbMixin> declaredMixins,
    PbMixin? defaultMixin,
  ) {
    final wellKnownMixin = wellKnownMixinForFullName(fullName);
    if (wellKnownMixin != null) return wellKnownMixin;
    if (!_descriptor.hasOptions() ||
        !_descriptor.options.hasExtension(Dart_options.mixin)) {
      return defaultMixin;
    }

    final name = _descriptor.options.getExtension(Dart_options.mixin) as String;
    if (name.isEmpty) return null; // don't use any mixins (override default)
    final mixin = declaredMixins[name] ?? findMixin(name);
    if (mixin == null) {
      throw '${_descriptor.name} in ${parent.fileGen!.descriptor.name}: mixin "$name" not found';
    }
    return mixin;
  }
}
